/*
 * Copyright (c) 2011, B3log Team
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.b3log.latke.maven.plugin;

import freemarker.template.Configuration;
import freemarker.template.Template;
import java.io.File;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.b3log.latke.annotation.RequestProcessing;
import org.json.JSONObject;

/**
 * Goal which generates web deployment descriptor (web.xml) and dispatching 
 * sources.
 * 
 * @goal assemble
 * @phase process-classes
 * @requiresDependencyResolution compile
 * 
 * @author <a href="mailto:DL88250@gmail.com">Liang Ding</a>
 * @version 1.0.0.1, Jul 17, 2011
 */
public final class AssemblerMojo extends AbstractMojo {

    /**
     * Base directory.
     * 
     * @parameter expression="${min.cssSourceDir}" default-value="${basedir}"
     */
    private String baseDir;
    /**
     * Target directory.
     * 
     * @parameter expression="${min.jsTargetDir}" default-value="${project.build.directory}"
     */
    private String targetDir;
    /**
     * Project classpath.
     * 
     * @parameter default-value="${project.compileClasspathElements}"
     * @required
     * @readonly
     */
    private List<String> classpathElements;
    /**
     * Canonical name of annotation {@linkplain RequestProcessing}.
     */
    private static final String REQUEST_PROCESSING_ANNOTATION_NAME =
            RequestProcessing.class.getCanonicalName();
    /**
     * FreeMarker {@linkplain  Configuration configuration}.
     */
    private Configuration configuration;

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        unpackTemplateDir();
        final Log log = getLog();
        // final ExecutorService executor = Executors.newFixedThreadPool(2);
        try {
            getRequestProcessorClasses();
        } catch (final Exception e) {
            log.error(e);

            throw new MojoExecutionException(e.getMessage(), e);
        }
    }

    /**
     * Unpacks template directory.
     */
    private void unpackTemplateDir() {
        configuration = new Configuration();
        configuration.setDefaultEncoding("UTF-8");

        try {
            final URL resource =
                    AssemblerMojo.class.getClassLoader().getResource("/template");
            final String jarPath =
                    StringUtils.substringBetween(resource.getPath(),
                                                 "file:/", "!");

            final String jarDir = FilenameUtils.getFullPath(jarPath);
            final String targetDirPath = jarDir + File.separator
                                         + "template";
            final File templateDir = new File(targetDirPath);
            templateDir.mkdir();

            final InputStream resourceStream =
                    AssemblerMojo.class.getClassLoader().getResourceAsStream(
                    "/template/HTTPRequestDispatcher.ftl");

            final String content = IOUtils.toString(resourceStream);

            final String targetFilePath = targetDirPath + File.separator
                                          + "HTTPRequestDispatcher.ftl";
            final FileWriter targetWriter = new FileWriter(targetFilePath);
            IOUtils.write(content, targetWriter);
            targetWriter.close();

            configuration.setDirectoryForTemplateLoading(templateDir);
        } catch (final Exception e) {
            System.err.println(e);
            throw new RuntimeException(e);
        }
    }

    public Set<Class<?>> getRequestProcessorClasses() throws Exception {
        final Log log = getLog();

        final Set<Class<?>> ret = new HashSet<Class<?>>();

        // TODO: request process classes discovery

        final URL[] urls = new URL[classpathElements.size()];
        for (int i = 0; i < classpathElements.size(); i++) {
            final File classpath = new File(classpathElements.get(i));
            urls[i] = classpath.toURI().toURL();
        }

        final URLClassLoader classLoader = new URLClassLoader(urls);
        final String className = "org.b3log.latke.demo.hello.HelloProcessor";
        final Class<?> clazz = classLoader.loadClass(className);

        final Set<JSONObject> requestProcessingClasses =
                new HashSet<JSONObject>();

        log.debug("Loading class [" + clazz + "]");
        final JSONObject processingClass = new JSONObject();
        requestProcessingClasses.add(processingClass);
        processingClass.put("classSimpleName", clazz.getSimpleName());
        processingClass.put("classCanonicalName", clazz.getCanonicalName());

        final Set<JSONObject> processingMethods = new HashSet<JSONObject>();
        processingClass.put("processingMethods", (Object) processingMethods);

        final Method[] methods = clazz.getDeclaredMethods(); // A request processor should not a subclass
        for (int i = 0; i < methods.length; i++) {
            final Method method = methods[i];

            final Annotation[] annotations = method.getDeclaredAnnotations();
            for (int j = 0; j < annotations.length; j++) {
                final Annotation annotation = annotations[j];

                // XXX: Why can not process these annotations normally (method.isAnnotationPresent(annotationClass))?

                if (REQUEST_PROCESSING_ANNOTATION_NAME.equals(
                        annotation.annotationType().getCanonicalName())) {
                    log.debug("Found a request processing method["
                              + method + ", RequestProcessing="
                              + annotation.toString() + "]");
                    final Map<String, String> props =
                            parseRequestProcessing(annotation);
                    final String requestURI = props.get("value");
                    final String requestMethod = props.get("method");
                    log.debug("Parsed request processing[value="
                              + requestURI + ", method=" + requestMethod
                              + "]");

                    final JSONObject processingMethod = new JSONObject();
                    processingMethod.put("requestURI", requestURI);
                    processingMethod.put("method", requestMethod);
                    processingMethod.put("processingMethodName",
                                         method.getName());

                    processingMethods.add(processingMethod);
                }
            }
        }

        final Template dispatcherTemplate =
                configuration.getTemplate("HTTPRequestDispatcher.ftl");

        final Map<String, Object> dataModel = new HashMap<String, Object>();
        dataModel.put("requestProcessingClasses", requestProcessingClasses);

        final Writer writer = new OutputStreamWriter(System.out);
        dispatcherTemplate.process(dataModel, writer);

        return ret;
    }

    /**
     * Parses the specified request processing.
     * 
     * @param requestProcessing the specified request processing
     * @return parsed mappings, for example, 
     * <pre>
     * &lt;"value", "/default.html"&gt;
     * &lt;"method", "GET, POST"&gt;
     * <pre>
     */
    private Map<String, String> parseRequestProcessing(
            final Annotation requestProcessing) {
        final Map<String, String> ret = new HashMap<String, String>();

        String requestProcessingString = requestProcessing.toString();

        String value =
                StringUtils.substringBetween(requestProcessingString,
                                             "value=[", "]");
        ret.put("value", value);

        requestProcessingString = StringUtils.remove(requestProcessingString,
                                                     "value=[" + value + "], ");
        final String method =
                StringUtils.substringBetween(requestProcessingString,
                                             "method=[", "]");
        ret.put("method", method);

        return ret;
    }
}
